package client.logic;

//TODO readme
//TODO make jars and pack up the work

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;


import packets.Packet;
import packets.client.AllUsersPacket;
import packets.client.ChatPacket;
import packets.client.DeletePacket;
import packets.client.DetailsPacket;
import packets.client.Login;
import packets.client.LogoutPacket;
import packets.client.PublicKeyPair;
import packets.client.Registration;
import packets.client.WhosOnlinePacket;
import packets.server.Certificate;
import packets.server.CertificateData;
import packets.server.UserDetails;
import packets.server.UserList;

import client.data.Identity;
import client.logic.chat.ChatStarter;
import client.logic.chat.ChatWaiter;
import client.logic.chat.Chatter;
import encription.SecurityHandler;
import exceptions.FileTooBigException;
import exceptions.InvalidPacketContentException;
import exceptions.RegistrationExeption;
import exceptions.UnverifiedMessageException;

/**
 * performs all the client logic operations
 */
public class Controller {

	private static final int DEFAULT_PORT = 1461;
	private static String SERVER_IP = "132.72.41.137";
	//private static final String SERVER_IP = "192.168.1.6";
	private Socket _serverSocket;
	private BufferedOutputStream _serverOut;
	private BufferedInputStream _serverIn;
	private String _currUser;
	private ChatWaiter _chatter;
	private boolean _chatting = false;
	private Identity _identity;
	private PublicKeyPair _serverKeys;
	private Certificate _cf;
	private ChatStarter _chatStarter;

	/**
	 * creates an instance of Controller and reads the server public keys
	 */
	public Controller(){
		try {
			_serverKeys = PublicKeyPair.readServerKeys();
		} catch (IOException e) {
			GuiFacade.getInstance().showErrorMessage("Error reading server public keys");
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			GuiFacade.getInstance().showErrorMessage("Error reading server public keys");
			e.printStackTrace();
		}
	}

	/**
	 * connects the client with the server , used when trying to register/login
	 * @throws UnknownHostException if the IP is invalid
	 * @throws IOException if there was a connection error
	 */
	private void connect() throws UnknownHostException, IOException{
		_serverSocket = new Socket(SERVER_IP, DEFAULT_PORT);
		_serverOut = new BufferedOutputStream(_serverSocket.getOutputStream());
		_serverOut.flush();
		_serverIn = new BufferedInputStream(_serverSocket.getInputStream());
	}

	/**
	 * closes the connection with the server
	 * @throws IOException
	 */
	private void disconnect() throws IOException{
		_serverIn.close();
		_serverOut.close();
		_serverSocket.close();
		_chatter.close();
	}

	/**
	 * performs the login operation with the server
	 * connects to the server by reading the client data and keys from the client file and sends the login request to the server
	 * stores the server reply which is a Certificate
	 * initializes the chat listener
	 * @param name client's name
	 * @param password clients's password (not encrypted)
	 */
	public void login(String name, char[] password) {
		try {
			connect(); 										//connect to the server
			_identity = (Identity) SecurityHandler.readFromFile("./clientfile-" + name); 		//retrieve client's identity from file
			Login login = new Login(_identity.getId(), name , SecurityHandler.encryptPassword(password));
			sendEncryptedMessage(new Packet(login));		//sends the login request to the server
			Packet feedback = getMessage();					//receives the server's reply
			if (feedback.getType()==Packet.CERTIFICATE){ 	//reply is a Certificate - successful login
				_currUser= name;							//sets the user as logged in
				_cf = feedback.getCertificateContent();		//saves the certificate
				_chatter = new ChatWaiter(_identity.getDecryptionKey(),_serverKeys.getSigkey(), this);	//initialize chat request receiver
				_chatter.start();
				GuiFacade.getInstance().showMainScreen(); //shows the user panel
			}else{											//reply is a string - login error
				GuiFacade.getInstance().showErrorMessage("Error , reply from server: "+ (feedback.getType() == Packet.STRING_MSG ? feedback.getStringContent() : null));
			}
		} catch (Exception e){
			SecurityHandler.catchException("logging in", e, true);
		}
	}

	/**
	 * sends a logout request to the server and disconnects
	 */
	public void logout(){
		try {
			sendEncryptedMessage(new LogoutPacket()); 		//sends logout request to the server
			getMessage();
		} catch (Exception e){
			SecurityHandler.catchException("logging out", e , true);
			//e.printStackTrace();
		}
		try{
			//disconnects from the server
			disconnect();
		} catch (IOException e) {
			GuiFacade.getInstance().showErrorMessage("Some error occured while logging out");
		}
		_currUser = null;
		//shows the login screen
		GuiFacade.getInstance().showLogin();
	}

	/**
	 * registers with the server
	 * generates the encryption keys and the user's identity
	 * stores the Certificate recieved from the server
	 * @param name user name
	 * @param password user's password (not encrypted)
	 * @param bday user birth day
	 * @param country user country
	 * @param city user city
	 */
	public void register(String name, char[] password , String bday , String country , String city , String img) {
		try {
			//check that image is in the right size before connecting with server
			File imgFile = new File(img);
			if (imgFile.length() > 6000) throw new FileTooBigException("image too big to be sent");

			connect(); //connects to the server

			String encryptedPassword = SecurityHandler.encryptPassword(password);
			//generates keys and receive keys from server
			//initializes the user's Identity
			_cf = doInitialRegistrationConnection(name, encryptedPassword);

			//sends a registration request to the server

			Registration reg = new Registration(name ,encryptedPassword , bday , country, city ,imgFile);// new File("./client-"+name));

			sendEncryptedMessage(new Packet(reg));

			Packet feedback = getMessage(); //receive the server's reply

			if (feedback.getType()==Packet.CERTIFICATE){					//certificate reply - successful registration
				_currUser = name; 											//sets the user as logged in
				_chatter = new ChatWaiter(_identity.getDecryptionKey(),_serverKeys.getSigkey(), this); 	//initialize chat request receiver
				_chatter.start();
				GuiFacade.getInstance().showMainScreen(); 					//shows the user panel
			}
			else {															//string reply - registration error
				GuiFacade.getInstance().showErrorMessage("Error , reply from server: "+ (feedback.getType() == Packet.STRING_MSG ? feedback.getStringContent() : null));
			}
		}catch (Exception e) {
			SecurityHandler.catchException("registering with server", e , true);
			//e.printStackTrace();
		}
	}

	/**
	 * sends the server a request to delete the user and returns to the main screen
	 */
	public void delete(){
		String tmpname = _currUser;
		_currUser = null;
		try{
			sendEncryptedMessage(new DeletePacket()); 					//send delete request to the server
			Packet feedback = getMessage();						 		//receives the server's response
			if (feedback.getType() != Packet.OK){
				GuiFacade.getInstance().showErrorMessage("Error while deleting");
			}else {
				File f = new File("./clientfile-"+tmpname);
				System.out.println(f.getAbsolutePath());
				f.delete();
			}
		} catch (Exception e){
			SecurityHandler.catchException("deleting user", e , true);
		}
		try{
			disconnect();						//disconnects from the server
		} catch (IOException e) {
			GuiFacade.getInstance().showErrorMessage("Some error occured while deleting user");
		}
		GuiFacade.getInstance().showLogin(); 	//returns to the main screen
	}

	/**
	 * ask the server for a list of the online users
	 */
	public void whosOnline(){
		try {
			sendEncryptedMessage(new WhosOnlinePacket());		//sends the server a whosonline request
			UserList users = getUserListReply();				//receive the server's reply (which is a list of the online users)
			GuiFacade.getInstance().showWhoIsOnline(users);		//shows the online users in a window
		} catch (Exception e){
			SecurityHandler.catchException("requesting whos online list", e , true);
		}
	}

	/**
	 * requests the server for a list of all users
	 */
	public void allUsers(){
		try {
			sendEncryptedMessage(new AllUsersPacket());		//sends the server an allusers request
			UserList users = getUserListReply();			//receive the server's reply (which is a list of the users)
			GuiFacade.getInstance().showAllUsers(users);	//shows the users in a window
		} catch (Exception e){
			SecurityHandler.catchException("requesting all users list", e , true);
		}
	}

	/**
	 * sends the server a request to chat with a friend
	 * receives the friend's certificate and connects with him
	 * @param id name of the friend to chat with
	 */
	public void chat(Integer id ,String name){
		try {
			sendEncryptedMessage(new ChatPacket(id));//sends the chat request to the server
			Packet feedback = getMessage();						//receive the server's reply - which is the Certificate of the requested user
			if (feedback.getType()==Packet.CERTIFICATE){
				if (!_chatting){								//cannot request a chat while already chatting , in the future users will be able to initiate multiple chats
					_chatting = true;
					CertificateData certificate = feedback.getCertificateContent().getData(_serverKeys.getSigkey());
					InetSocketAddress ip_address = new InetSocketAddress(certificate.getIp(), Chatter.DEFAULT_CHAT_PORT/*feedback.getPort()*/); //other client's IP address and port
					PublicKey encryptionKey = certificate.getKey();	//other client's public key
					_chatStarter = new ChatStarter(ip_address, name, _identity.getDecryptionKey(),encryptionKey,_cf, this);	//initiate the chat with the other user
					_chatStarter.start();
				}else{
					GuiFacade.getInstance().showErrorMessage("already chatting");
				}
			} else {
				GuiFacade.getInstance().showErrorMessage("error while trying to chat , server did not send a certificate - " + (feedback.getType() == Packet.STRING_MSG ? feedback.getStringContent() : null));
			}
		} catch (Exception e){
			SecurityHandler.catchException("trying to chat", e , true);
		}
	}

	/**
	 * sends the server a request for user details
	 * receives back the user's certificate
	 * @param id the user id for the details request
	 */
	public void details(Integer id){
		try {
			sendEncryptedMessage(new DetailsPacket(id));//send details request to the server
			UserDetails feedback = getDetailsReply();	//receive the server's reply
			//TODO user gets a certificate now , but he does nothing with it - make certificate cache
			if (feedback != null){
				GuiFacade.getInstance().showUserDetails(feedback);		//shows the user details in a screen
			}else{
				GuiFacade.getInstance().showErrorMessage("no certificate for user");
			}
		} catch (Exception e){
			SecurityHandler.catchException("getting user details", e , true);
		}
	}

	/**
	 * @return the user that is currently logged in with the current client
	 */
	public String getLoggedUser() {
		return _currUser;
	}

	/**
	 * notifies the controller that the chat the user start was ended , so the user can initiate a new chat
	 * (the user is allowed to start only one chat)
	 */
	public void resetChat() {
		if (_chatting){
			_chatting = false;
			_chatStarter = null;
		}
	}

	/**
	 * notifies the controller that the chat the user received was ended , so the user can receive a new chat
	 * (the user is allowed to receive only one chat)
	 */
	public void resetChatWaiter() {
		System.out.println("resetting chat waiter");
		_chatter = new ChatWaiter(_identity.getDecryptionKey(),_serverKeys.getSigkey(), this);
		_chatter.start();
	}

	/**
	 * closes the chat waiter
	 */
	public void closeChatWaiter(){
		if (_chatter!=null)
			_chatter.close();
	}

	/**
	 * closes the chat starter (if exists)
	 */
	public void closeChatStarter(){
		if (_chatStarter!=null){
			_chatStarter.close();
		}
	}

	/**
	 * sends a Packet to the server , encrypted and signed.
	 * @param packet the packet to send
	 * @throws Exception if there was a problem encrypting/signing the message or communicating with the server
	 */
	private void sendEncryptedMessage(Packet packet) throws Exception{
		SecurityHandler.sendEncryptedMessage(packet, _serverKeys.getEncryptionKey(), _identity.getSignatureKey(), _serverOut);
	}

	/**
	 * receives a packet from the server,  which contains a message of various types (see packet class)
	 * the packet will be decrypted using the clients identity key
	 * @return the Packet received from the server
	 * @throws Exception if there was an error while verifying or decrypting the Packet , or there was a communicatino error with the server
	 */
	private Packet getMessage() throws Exception{
		return SecurityHandler.getMessage(_serverKeys.getSigkey(), _identity.getDecryptionKey(), _serverIn);
	}

	/**
	 * receives a UserList message from the server , which contains a list of user ids and their names
	 * @return the UserList received from the server
	 * @throws Exception if there was an exception while verifying the message , or receiving the data from the server
	 */
	private UserList getUserListReply() throws Exception{
		Packet packet = getMessage();
		return packet.getUserListContent();
	}


	/**
	 * receives a Certificate from the server
	 * @param privateKey private key to use to decrypt the message
	 * @return the Certificate which the server sent
	 * @throws Exception if there was an error while verifying or decrypting , or there was a communication error with the server
	 */
	private Certificate getCertificateReply(PrivateKey privateKey) throws Exception {
		Packet packet = SecurityHandler.getMessage(_serverKeys.getSigkey(), privateKey, _serverIn);
		return packet.getCertificateContent();
	}

	/**
	 * receives a UserDetails entry from the server (decrypted and verified)
	 * @return the user details which the server sent
	 * @throws UnverifiedMessageException if the server's signature was not verified
	 * @throws InvalidPacketContentException if the recived packet content wasnt of UserDetails type
	 * @throws Exception if there was an error while verifying or decrypting , or there was a communication error with the server
	 */
	private UserDetails getDetailsReply() throws  UnverifiedMessageException , InvalidPacketContentException , Exception{
		Packet packet = getMessage();
		return packet.getDetailsContent();
	}

	/**
	 * registers client ID with the server for the first time
	 * generates client keys , receive server keys from the server , sends the client public keys to the server, receives client ID ,and writes the client's Identity to a file
	 * @param name client's name
	 * @param encryptedPassword client's encrypted password
	 * @throws Exception if there was a problem in the registration process
	 */
	private Certificate doInitialRegistrationConnection(String name , String encryptedPassword) throws Exception{
		KeyPair myMessagingKey = SecurityHandler.makeKey();
		KeyPair digitalSignature = SecurityHandler.makeKey();
		//send client keys to server
		PublicKeyPair content= new PublicKeyPair(myMessagingKey.getPublic(), digitalSignature.getPublic());
		//create packet of public keys type
		Packet packet = new Packet(content);
		//encrypt keys with the server key and send them signed to the server
		SecurityHandler.sendEncryptedMessage(packet, _serverKeys.getEncryptionKey(), digitalSignature.getPrivate(), _serverOut);
		//receive client id from server , should be signed
		Certificate reply = getCertificateReply(myMessagingKey.getPrivate());
		if (reply!= null){
			int clientKey = reply.getData(_serverKeys.getSigkey()).getId();
			_identity = new Identity(clientKey, encryptedPassword, digitalSignature , myMessagingKey);
			SecurityHandler.writeToFile(_identity, "./clientfile-" + name);
			return reply;
		} else {
			throw new RegistrationExeption("bad client key response from server");
		}
	}
	/**
	 * checks with the server that a key is not in the CRL
	 * @param pk the key to check
	 * @return true if the key is valid
	 * @throws Exception of there was a problem communicating with the server
	 */
	public Packet verifyKey(PublicKey pk) throws Exception{
		sendEncryptedMessage(new Packet(pk));
		return getMessage();
	}
	
	/**
	 * sets the server IP
	 * @param ip server's IP
	 */
	public static void setServerIp(String ip){
		SERVER_IP = ip;
	}

}
